package com.stone.db.dbsecurity.helper;

import com.stone.db.dbsecurity.BaseSecurityHandler;
import com.stone.db.dbsecurity.constant.SymbolConstant;
import com.stone.db.dbsecurity.uitl.PropertiesUtil;
import com.stone.db.dbsecurity.uitl.StringUtil;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Created by 喵 on 2018/3/27.
 */
public final class InterceptorHelper {

    /**
     * 表名列表
     */
    private Set<String> tableNames = new HashSet<>(10);
    /**
     * 加密字段
     */
    private Map<String, List<String>> encryptColumns;
    /**
     * 解密字段
     */
    private Map<String, List<String>> decryptColumns;
    /**
     * 加密服务器
     */
    private BaseSecurityHandler baseSecurityHandler;


    /**
     * @param ms
     * @param parameterObject
     * @return
     * @throws UnsupportedEncodingException
     */

    public Map<Integer, String> encrypt(final MappedStatement ms, final Object parameterObject,String tableName) throws UnsupportedEncodingException {

        if (this.encryptColumns.size() <= 0) {
            return null;
        }
        Map<Integer, String> valueMap = new HashMap<>(10);

        final SqlSource sqlSource = ms.getSqlSource();

        final Configuration configuration = ms.getConfiguration();

        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

        final BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

        final List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        for (int $i = 0; $i < parameterMappings.size(); $i++) {
            final ParameterMapping parameterMapping = parameterMappings.get($i);
            final String propertyName = parameterMapping.getProperty();
            Object value;
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                if (this.encryptColumns.get(tableName).contains(propertyName)) {
                    value = metaObject.getValue(propertyName);
                    metaObject.setValue(propertyName, this.baseSecurityHandler.encrypt(value.toString()));
                }
                value = metaObject.getValue(propertyName);
            }
            valueMap.put($i, sqlFomartValue(value));
        }
        return valueMap;

    }


    /**
     * decrypt
     *
     * @param result
     */
    public void decrypt(Object result,String tableName) throws Exception {
        if (this.decryptColumns.size() <= 0) {
            return;
        }
        if (result instanceof ArrayList) {
            final List resultList = (ArrayList) result;
            for (int i = 0; i < resultList.size(); i++) {
                final Object object = resultList.get(i);
                if (object instanceof Map) {
                    final Map<String, Object> map = (HashMap) object;
                    for (Map.Entry<String, Object> entry : map.entrySet()) {
                        String property = entry.getKey();
                        Object value = entry.getValue();
                        if (this.decryptColumns.get(tableName).contains(property)) {
                            final String val = (String) value;
                            entry.setValue(baseSecurityHandler.decrypt(val));
                        }
                    }
                } else {
                    final Class<?> clazz = object.getClass();
                    final Field[] fields = clazz.getDeclaredFields();
                    for (Field field : fields) {
                        field.setAccessible(true);
                        final String property = field.getName();
                        final Object value = field.get(object);
                        if (this.decryptColumns.get(tableName).contains(property)) {
                            final String val = (String) value;
                            field.set(object, baseSecurityHandler.decrypt(val));
                        }
                    }
                }
            }
        }
    }


    /**
     * @param value
     * @return
     */
    private String sqlFomartValue(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof java.lang.Number || value instanceof java.lang.Boolean || value instanceof java.math.BigDecimal) {
            return String.valueOf(value);
        }
        if (value instanceof java.lang.String) {
            return "'" + String.valueOf(value) + "'";
        }
        if (value instanceof java.util.Date) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return "'" + sdf.format((Date) value) + "'";
        }
        return String.valueOf(value);
    }

    /**
     * @param sqlWithQuestionMark
     * @param paramMap
     * @return
     */

    private static String sqlReplaceByParameter(String sqlWithQuestionMark, final Map<Integer, String> paramMap) {
        for (Map.Entry<Integer, String> entry : paramMap.entrySet()) {
            sqlWithQuestionMark = sqlWithQuestionMark.replaceFirst("\\?", entry.getValue());
        }
        return sqlWithQuestionMark;
    }


    /**
     * 是否配置的表名
     *
     * @param tableName
     * @return
     */
    public boolean isAllowTable(String tableName) {
        if (this.tableNames.size() <= 0) {
            return false;
        }
        return this.tableNames.contains(tableName);
    }

    /**
     * 解析配置文件
     *
     * @param pro
     */
    public void parseProperties(Properties pro) {
        try {
            final String className = pro.getProperty(SymbolConstant.SECURITY_HANDLER_CLASS, "");
            if (!StringUtil.isEmpty(className)) {
                final Class<?> clazz = Class.forName(className);
                this.baseSecurityHandler = (BaseSecurityHandler) clazz.newInstance();
            }
            String encryptStr = pro.getProperty(SymbolConstant.TABLE_COLUMN_ENCRYPT, "");
            this.encryptColumns = PropertiesUtil.parseTableColumn(encryptStr, SymbolConstant.TABLE_COLUMN_ENCRYPT,tableNames);
            String decryptStr = pro.getProperty(SymbolConstant.TABLE_COLUMN_DECRYPT, "");
            this.decryptColumns = PropertiesUtil.parseTableColumn(decryptStr, SymbolConstant.TABLE_COLUMN_DECRYPT,tableNames);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

    /**
     * @param invocation
     * @param ms
     * @param insertParam
     * @return
     * @throws Exception
     */
    public Object getResult(Invocation invocation, MappedStatement ms, Object insertParam,String tableName) throws Exception {
        Object result;
        switch (ms.getSqlCommandType()) {
            case INSERT:
                this.encrypt(ms, insertParam,tableName);
                result = invocation.proceed();
                break;
            case UPDATE:
                this.encrypt(ms, insertParam,tableName);
                result = invocation.proceed();
                break;
            case SELECT:
                result = invocation.proceed();
                this.decrypt(result,tableName);
                break;
            default:
                result = invocation.proceed();
                break;
        }
        return result;
    }
}
